﻿Imports System.Net
Imports System.Text
Imports System.Web
Imports System.Runtime.Serialization
Imports System.Xml
Imports System.IO
Imports Sgml

Partial Class TwitterClient
    ''' <summary>
    ''' タイムラインの種類を表現します。
    ''' </summary>
    ''' <remarks></remarks>
    Private Enum TimelineType
        [Public]
        Home
        Replies
        Inbox
        Sent
        User
    End Enum

    ''' <summary>
    ''' タイムラインを取得した結果として返されるJSONを格納します。
    ''' </summary>
    ''' <remarks></remarks>
    <DataContract()> _
    Private Class GetTimeLineJsonResult
        Private _pagination As String
        <DataMember(Name:="#pagination")> _
        Public Property Pagination() As String
            Get
                Return _pagination
            End Get
            Set(ByVal value As String)
                _pagination = value
            End Set
        End Property

        Private _timeline As String
        <DataMember(Name:="#timeline")> _
        Public Property Timeline() As String
            Get
                Return _timeline
            End Get
            Set(ByVal value As String)
                _timeline = value
            End Set
        End Property
    End Class

    Private _timelinePath As New Dictionary(Of TimelineType, String)

    Private Sub InitializeTimeline(ByVal sender As Object, ByVal e As EventArgs) Handles Me.OnInitialize
        _timelinePath.Add(TimelineType.Public, "/public_timeline")
        _timelinePath.Add(TimelineType.Home, "/home")
        _timelinePath.Add(TimelineType.Replies, "/replies")
        _timelinePath.Add(TimelineType.Inbox, "/inbox")
        _timelinePath.Add(TimelineType.Sent, "/sent")
        _timelinePath.Add(TimelineType.User, "/{0}")
    End Sub

    ''' <summary>
    ''' パブリック(Public)タイムラインを取得します。
    ''' </summary>
    ''' <param name="maxPage">取得する最大ページ数</param>
    ''' <param name="lastStatus">取得済みの最新のステータス</param>
    ''' <returns>TwitterStatusのコレクション</returns>
    ''' <remarks></remarks>
    Public Function GetPublicTimeline(ByVal maxPage As Integer, ByVal lastStatus As TwitterStatus) As IList(Of TwitterStatus)
        Return GetPublicTimeline(maxPage, lastStatus.StatusID)
    End Function

    ''' <summary>
    ''' パブリック(Public)タイムラインを取得します。
    ''' </summary>
    ''' <param name="maxPage">取得する最大ページ数</param>
    ''' <param name="lastStatusID">取得済みの最新のステータスの固有ID</param>
    ''' <returns>TwitterStatusのコレクション</returns>
    ''' <remarks></remarks>
    Public Function GetPublicTimeline(ByVal maxPage As Integer, ByVal lastStatusID As Long) As IList(Of TwitterStatus)
        Return GetTimeline(maxPage, lastStatusID, TimelineType.Public, Nothing)
    End Function

    ''' <summary>
    ''' ホーム(Recent/Friends)タイムラインを取得します。
    ''' </summary>
    ''' <param name="maxPage">取得する最大ページ数</param>
    ''' <param name="lastStatus">取得済みの最新のステータス</param>
    ''' <returns>TwitterStatusのコレクション</returns>
    ''' <remarks></remarks>
    Public Function GetHomeTimeline(ByVal maxPage As Integer, ByVal lastStatus As TwitterStatus) As IList(Of TwitterStatus)
        Return GetHomeTimeline(maxPage, lastStatus.StatusID)
    End Function

    ''' <summary>
    ''' ホーム(Recent/Friends)タイムラインを取得します。
    ''' </summary>
    ''' <param name="maxPage">取得する最大ページ数</param>
    ''' <param name="lastStatusID">取得済みの最新のステータスの固有ID</param>
    ''' <returns>TwitterStatusのコレクション</returns>
    ''' <remarks></remarks>
    Public Function GetHomeTimeline(ByVal maxPage As Integer, ByVal lastStatusID As Long) As IList(Of TwitterStatus)
        Return GetTimeline(maxPage, lastStatusID, TimelineType.Home, Nothing)
    End Function

    ''' <summary>
    ''' 返信(Replies/Memento)タイムラインを取得します。
    ''' </summary>
    ''' <param name="maxPage">取得する最大ページ数</param>
    ''' <param name="lastStatus">取得済みの最新のステータス</param>
    ''' <returns>TwitterStatusのコレクション</returns>
    ''' <remarks></remarks>
    Public Function GetRepliesTimeline(ByVal maxPage As Integer, ByVal lastStatus As TwitterStatus) As IList(Of TwitterStatus)
        Return GetRepliesTimeline(maxPage, lastStatus.StatusID)
    End Function

    ''' <summary>
    ''' 返信(Replies/Memento)タイムラインを取得します。
    ''' </summary>
    ''' <param name="maxPage">取得する最大ページ数</param>
    ''' <param name="lastStatusID">取得済みの最新のステータスの固有ID</param>
    ''' <returns>TwitterStatusのコレクション</returns>
    ''' <remarks></remarks>
    Public Function GetRepliesTimeline(ByVal maxPage As Integer, ByVal lastStatusID As Long) As IList(Of TwitterStatus)
        Return GetTimeline(maxPage, lastStatusID, TimelineType.Replies, Nothing)
    End Function

    ''' <summary>
    ''' 受信ダイレクトメッセージ(Inbox)タイムラインを取得します。
    ''' </summary>
    ''' <param name="maxPage">取得する最大ページ数</param>
    ''' <param name="lastStatus">取得済みの最新のステータス</param>
    ''' <returns>TwitterStatusのコレクション</returns>
    ''' <remarks></remarks>
    Public Function GetInboxTimeline(ByVal maxPage As Integer, ByVal lastStatus As TwitterStatus) As IList(Of TwitterStatus)
        Return GetInboxTimeline(maxPage, lastStatus.StatusID)
    End Function

    ''' <summary>
    '''  受信ダイレクトメッセージ(Inbox)タイムラインを取得します。
    ''' </summary>
    ''' <param name="maxPage">取得する最大ページ数</param>
    ''' <param name="lastStatusID">取得済みの最新のステータスの固有ID</param>
    ''' <returns>TwitterStatusのコレクション</returns>
    ''' <remarks></remarks>
    Public Function GetInboxTimeline(ByVal maxPage As Integer, ByVal lastStatusID As Long) As IList(Of TwitterStatus)
        Return GetTimeline(maxPage, lastStatusID, TimelineType.Inbox, Nothing)
    End Function

    ''' <summary>
    ''' 送信ダイレクトメッセージ(Sent)タイムラインを取得します。
    ''' </summary>
    ''' <param name="maxPage">取得する最大ページ数</param>
    ''' <param name="lastStatus">取得済みの最新のステータス</param>
    ''' <returns>TwitterStatusのコレクション</returns>
    ''' <remarks></remarks>
    Public Function GetSentTimeline(ByVal maxPage As Integer, ByVal lastStatus As TwitterStatus) As IList(Of TwitterStatus)
        Return GetSentTimeline(maxPage, lastStatus.StatusID)
    End Function

    ''' <summary>
    ''' 送信ダイレクトメッセージ(Sent)タイムラインを取得します。
    ''' </summary>
    ''' <param name="maxPage">取得する最大ページ数</param>
    ''' <param name="lastStatusID">取得済みの最新のステータスの固有ID</param>
    ''' <returns>TwitterStatusのコレクション</returns>
    ''' <remarks></remarks>
    Public Function GetSentTimeline(ByVal maxPage As Integer, ByVal lastStatusID As Long) As IList(Of TwitterStatus)
        Return GetTimeline(maxPage, lastStatusID, TimelineType.Sent, Nothing)
    End Function

    ''' <summary>
    ''' ユーザーのタイムラインを取得します。
    ''' </summary>
    ''' <param name="maxPage">取得する最大ページ数</param>
    ''' <param name="lastStatus">取得済みの最新のステータス</param>
    ''' <returns>TwitterStatusのコレクション</returns>
    ''' <remarks></remarks>
    Public Function GetUserTimeline(ByVal maxPage As Integer, ByVal lastStatus As TwitterStatus, ByVal user As TwitterUser) As IList(Of TwitterStatus)
        Return GetUserTimeline(maxPage, lastStatus.StatusID, User.UserName)
    End Function

    ''' <summary>
    ''' ユーザーのタイムラインを取得します。
    ''' </summary>
    ''' <param name="maxPage">取得する最大ページ数</param>
    ''' <param name="lastStatusID">取得済みの最新のステータスの固有ID</param>
    ''' <returns>TwitterStatusのコレクション</returns>
    ''' <remarks></remarks>
    Public Function GetUserTimeline(ByVal maxPage As Integer, ByVal lastStatusID As Long, ByVal userName As String) As IList(Of TwitterStatus)
        Return GetTimeline(maxPage, lastStatusID, TimelineType.User, userName)
    End Function

    Private Function GetTimeline(ByVal maxPage As Integer, ByVal lastStatusID As Long, ByVal type As TimelineType) As IList(Of TwitterStatus)
        Return GetTimeline(maxPage, lastStatusID, type, Nothing)
    End Function

    Private Function GetTimeline(ByVal maxPage As Integer, ByVal lastStatusID As Long, ByVal type As TimelineType, ByVal userName As String) As IList(Of TwitterStatus)
        If Not IsLogined Then
            Throw New InvalidOperationException(StrThisInstanceIsNotLoginedYet)
        End If
        If type = TimelineType.User AndAlso userName Is Nothing Then
            Throw New ArgumentNullException("userName", "typeがTimelineType.Userの場合userNameにNullを指定することはできません。")
        End If

        Dim lstStatus As New List(Of TwitterStatus)
        Dim maxId As Long = 0
        Dim pageNo As Integer = 1
        Dim user As TwitterUser = Nothing
        If (type = TimelineType.User) Then
            user = GetUserForTimeline(userName)
            If user Is Nothing Then
                Return Nothing
            End If
        End If
        While pageNo <= maxPage
            Dim url As String
            url = String.Format("{0}{1}?twttr=true&page={2}", My.Resources.TwitterUrl, String.Format(_timelinePath(type), userName), pageNo)
            If (maxId > 0) Then
                url += String.Format("&max_id={0}", maxId)
            End If
            Dim req As HttpWebRequest = DirectCast(WebRequest.Create(url), HttpWebRequest)
            req.SetTwitterXHRGetHeaders(String.Format("{0}{1}", My.Resources.TwitterUrl, _timelinePath(type)), _cookies)
            Dim jsonResult As GetTimeLineJsonResult
            Using res As HttpWebResponse = DirectCast(req.GetResponse(), HttpWebResponse)
                Select Case res.StatusCode
                    Case HttpStatusCode.OK
                        jsonResult = res.GetResponseAsJson(Of GetTimeLineJsonResult)()
                    Case Else
                        Return Nothing
                End Select
            End Using
            Dim xmlRes As XmlDocument = Nothing
            If String.IsNullOrEmpty(jsonResult.Timeline) Then
                Return Nothing
            End If
            xmlRes.LoadSgml(jsonResult.Timeline)
            Dim statusListItems As XmlNodeList = xmlRes.SelectNodes("ol[@id='timeline']/li")
            For Each node As XmlNode In statusListItems
                Dim status As TwitterStatus
                If (type <> TimelineType.User) Then
                    status = ParseStatusListItem(node, type, Nothing)
                Else
                    status = ParseStatusListItem(node, type, user)
                End If
                If (status.StatusID = lastStatusID) Then
                    Exit While
                End If
                lstStatus.Add(status)
            Next
            If String.IsNullOrEmpty(jsonResult.Pagination) Then
                Exit While
            End If
            xmlRes.LoadSgml(jsonResult.Pagination)
            Dim pnAnchorNode As XmlNode = xmlRes.SelectSingleNode("/div[@id='pagination']/a")
            If pnAnchorNode Is Nothing Then
                Exit While
            End If
            Dim pnHref As String = HttpUtility.HtmlDecode(pnAnchorNode.Attributes("href").Value)
            If (maxId = 0) Then
                Dim bgnIdx As Integer = pnHref.IndexOf("?max_id=") + 8
                maxId = CLng(pnHref.Substring(bgnIdx, pnHref.IndexOf("&", bgnIdx) - bgnIdx))
            End If
            pageNo += 1
        End While
        Return lstStatus
    End Function

    ''' <summary>
    ''' レスポンスに含まれる投稿されたステータスを解析します。
    ''' </summary>
    ''' <param name="node">ステータスのルートノード</param>
    ''' <param name="type">タイムラインの種類</param>
    ''' <returns>解析されたステータス</returns>
    ''' <remarks></remarks>
    Private Shared Function ParseStatusListItem(ByVal node As XmlNode, ByVal type As TimelineType, ByVal user As TwitterUser) As TwitterStatus
        Dim status As TwitterStatus
        status = New TwitterStatus()
        status.CreatedAt = DateTime.Now 'TODO:Web経由では曖昧な値しか取れないため
        If type = TimelineType.Inbox Or type = TimelineType.Sent Then
            status.StatusID = CLng(node.Attributes("id").Value.Substring(15))
        Else
            status.StatusID = CLng(node.Attributes("id").Value.Substring(7))
        End If
        If (type <> TimelineType.User) Then
            Dim authorAnchorNode As XmlNode = node.SelectSingleNode("div[@class='groupable']/span/a[@class='url']")
            status.User.UserName = authorAnchorNode.Attributes("href").Value.Substring(19)
            Dim authorImageNode As XmlNode = authorAnchorNode.SelectSingleNode("img")
            status.User.DisplayName = authorImageNode.Attributes("alt").Value
            status.User.IconUrl = authorImageNode.Attributes("src").Value
            Dim bgnIndex As Integer = status.User.IconUrl.IndexOf("profile_images/")
            If (bgnIndex >= 0) Then
                bgnIndex += 15
                Dim len As Integer = status.User.IconUrl.IndexOf("/"c, bgnIndex) - bgnIndex
                'TODO:画像が設定されていない場合のUserID
                status.User.UserID = CLng(status.User.IconUrl.Substring(bgnIndex, len))
            End If
        Else
            status.User = user
        End If
        Dim statusBodyNode As XmlNode = node.SelectSingleNode("span[@class='status-body']")
        status.Text = statusBodyNode.SelectSingleNode("span[@class='entry-content']").InnerText
        Dim lockImageNode As XmlNode = statusBodyNode.SelectSingleNode("img[@class='lock']")
        If lockImageNode IsNot Nothing Then
            status.User.IsProtected = True
        End If
        Dim entryMetaNode As XmlNode = statusBodyNode.SelectSingleNode("span[@class='meta entry-meta']")
        If type <> TimelineType.Inbox And type <> TimelineType.Sent Then
            Dim fromNode = entryMetaNode.SelectSingleNode("span[contains(text(),'で')]")
            status.Source = fromNode.InnerText.TrimEnd("で"c)
            Dim replyToAnchorNode = entryMetaNode.SelectSingleNode("a[contains(text(),'宛')]")
            If replyToAnchorNode IsNot Nothing Then
                status.ReplyUserName = replyToAnchorNode.InnerText.TrimEnd("宛"c)
                status.ReplyStatusID = CLng(replyToAnchorNode.Attributes("href").Value.Substring(27 + status.ReplyUserName.Length))
            End If
            Dim favActionAnchorNode = node.SelectSingleNode("span[@class='actions']/div/a[contains(@class,'fav-action')]")
            status.IsFavorated = favActionAnchorNode.Attributes("class").Value.IndexOf("non") < 0
        End If
        Return status
    End Function

    Private Function GetUserForTimeline(ByVal userName As String) As TwitterUser
        Dim user As TwitterUser = New TwitterUser()
        Dim url As String = String.Format("{0}/{1}", My.Resources.TwitterUrl, userName)
        Dim req As HttpWebRequest = DirectCast(WebRequest.Create(url), HttpWebRequest)
        req.SetTwitterGetHeaders(Nothing, _cookies)
        Using res As HttpWebResponse = DirectCast(req.GetResponse(), HttpWebResponse)
            Select Case res.StatusCode
                Case HttpStatusCode.OK
                    res.LoadCookies(_cookies)
                    Dim xmlRes As XmlDocument = res.GetResponseAsXHtml()
                    Dim xnm As XmlNamespaceManager = xmlRes.GetXHtmlNameSpaceManager("x")
                    Dim followActionsNode As XmlNode = xmlRes.SelectSingleNode("//x:div[starts-with(@id,'follow_actions_')]", xnm)
                    user.UserID = CLng(followActionsNode.Attributes("id").Value.Substring(15))
                    user.UserName = followActionsNode.Attributes("class").Value.Substring(15)
                    Dim iconImageNode As XmlNode = xmlRes.SelectSingleNode("//x:div[@class='groupable']/x:h2//x:img", xnm)
                    user.IconUrl = iconImageNode.Attributes("src").Value
                    Dim displayNameNode As XmlNode = xmlRes.SelectSingleNode("//x:div[@id='profile']//x:ul/x:li/x:span[@class='fn']", xnm)
                    user.DisplayName = displayNameNode.InnerText.Trim
                Case Else
                    Return Nothing
            End Select
        End Using
        Return user
    End Function
End Class